const ELEMENT_NODE = 1;
const TEXT_NODE = 3;
const KeyMap = {
    enter: 'Enter',
    delete: ['Backspace', 'Delete'],//delete这个按钮修饰符对应2个按键
    up: 'ArrowUp',
    down: 'ArrowDown',
    left: 'ArrowLeft',
    right: 'ArrowRight',
    space: 'Space'
}
//dependency 依赖的意思，也就是被观察者，data对象的每个属性都对应一个Dep实例
//代表一个依赖，代表一个被观察的变量
class Dep {
    constructor() {
        this.subs = [];//观察者的数组
    }
    addSub(sub) {
        this.subs.push(sub);//添加一个新的观察者
    }
    notify() {//通知观察者更新
        this.subs.forEach(sub => sub.update());
    }
}
/**
 * 观察者,每一个用到data属性值的地方都对应一个观察者实例
 * 在data属性值发生变化的时候都要更新，都会收到通知，被调用update方法进行更新
 */
class Watcher {
    //监听的不但可以是表达式，也可是函数
    constructor(vm, exprOrFn, cb) {// exprOrFn fullName
        this.vm = vm;//观察哪个对象，在这里其实就是Vue类的实例
        this.cb = cb;//观察到变化后执行的回调函数
        //定义访问函数 
        this.getter = typeof exprOrFn === 'function' ? exprOrFn : getEvaluate(exprOrFn, vm);
        //只有在创建Watcher的时候才会给Dep.target,才会在访问属性的时候添加依赖
        //只有编译模板的时候才会创建Watcher实例，而编译模板只会编译一次
        Dep.target = this;//把当前的Watcher实例缓存到Dep.target,其实就是全局临时变量
        this.oldValue = this.getter();//访问vm的expr的属性值 vm.counter1
        Dep.target = null;
    }
    update() {
        //获取最新的值
        const newValue = this.getter();
        //如果新的值和老的值不一样
        //if (this.oldValue !== newValue) {
            //就会执行回调，更新DOM
            this.cb(newValue, this.oldValue);
            //把新的值保存给oldValue
            this.oldValue = newValue;
        //}
    }
}
//基于数组的原型创建一个新的数组原型对象
//不要直接修改数组的原型Array.prototype,会污染全局作用域
const arrayMethods = Object.create(Array.prototype);
['push','pop','shift','unshift','splice','sort','reverse'].forEach(methodName=>{
    //先获取原型上的原始方法
    const originalMethod = arrayMethods[methodName];
    //给这个新的原型对象定义新的方法
    Object.defineProperty(arrayMethods,methodName,{
        value:function(...args){
            let inserting = [];
            switch(methodName){
                case 'push':
                case 'unshift':
                    inserting=args;
                    break;
                case 'splice':
                    inserting = args.slice(2);    
                    break;
            }
            if(inserting.length>0){
                inserting.forEach(observe);
            }
            //先调用老的数组上的方法
            let result = originalMethod.apply(this,arguments);
            //通知数组的监听者们也就是watcher们更新
            this.__dep__.notify();
            return result;
        }
    });
});
function observe(value) {
    //如果value不存在，或者说value不是一个对象，直接返回,只观察数组和对象
    if (!value || typeof value !== 'object') {
        return;
    }
    if(Array.isArray(value)){
        //如果value是一个数组的话，重新指定它的原型对象 Array.prototype
        Object.setPrototypeOf(value,arrayMethods)
        value.forEach(observe);
    }else{
        for (let key in value) {
            defineReactive(value, key);
        }
    }
}
///obj data key users value [{id:1},{id:2}]
function defineReactive(obj, key, value=obj[key]) {
    observe(value);
    //为对象的每个属性创建一个Dep类的实例，通过这个实例实现收集观察者和通知观察者的功能
    let dep = new Dep();
    //如果value是数组的话，就可以给value添加自定义属性，__dep__,它指向此数组的依赖对象
    if(Array.isArray(value)){
        value.__dep__ = dep;
    }
    Object.defineProperty(obj, key, {
        get() {
            //如果Dep.target有值的话，把Dep.target也就是Watcher添加到此依赖的subs订阅列表里
            if (Dep.target) {
                dep.addSub(Dep.target);
            }
            return value;
        },
        set(newVal) {
            if (newVal !== value) {
                value = newVal;
                dep.notify();
            }
        }
    });
}
class Vue {
    static filters = {}
    static filter = (filterName, filterFunc) => {
        Vue.filters[filterName] = filterFunc;
    }
    constructor(options) {
        //保存选项
        this.$options = options;
        //通过选择器获取渲染的目标DOM元素
        this.$el = document.querySelector(options.el);
        //把选项里的data属性赋值给Vue实例的$data属性 
        this.$data = options.data;
        //接收用户定义的方法
        this.$methods = options.methods;
        //初始化
        this.init();
    }
    init() {
        //数据代理 vm.msg = options.data.msg
        this.proxyData();
        //把当前Vue的实例的$data属性对象里的属性变成可观察对象
        observe(this.$data);
        //方法代理vm.say = options.methods.say
        this.proxyMethods();
        //初始化计算属性
        this.initComputed();
        //初始化watch
        this.initWatch();
        //开始编译
        this.compile(this.$el);
    }
    initWatch() {
        //获取watch定义
        const { watch } = this.$options;
        //如果没有定义的话直接返回
        if (!watch) return;
        //如果有定义的话就获取对象的属性的数组
        const keys = Object.keys(watch);
        //遍历此数组
        keys.forEach(key => {
            //获取对应的值就是一个监听函数
            let value = watch[key];
            //创建一个观察者或者说监听者，监听Vue实例上key的变化，当变化后执行回调函数
            new Watcher(this, key, value.bind(this))
        })
    }
    //初始化计算属性
    initComputed() {
        //获取计算属性定义
        const { computed } = this.$options;//{fullName(){}}
        if (!computed) return;
        //返回对象的属性数组['fullName']
        const keys = Object.keys(computed);
        //遍历属性的数组
        keys.forEach(key => {
            //获取属性里的值
            const value = computed[key];
            let getter = value;
            let setter = () => console.warn(`计算属性无法重新赋值!`);
            if (typeof value === 'object') {
                getter = value.get;
                setter = value.set;
            }
            //给Vue实例对象添加属性，属性名叫fullName
            Object.defineProperty(this, key, {
                get: () => {//当取fullName属性的值的时候，获取计算函数，并计算得到结果
                    return getter.call(this)
                },
                set: (newValue) => {
                    setter.call(this, newValue);
                }
            });
        });
    }
    
    
    proxyMethods() {
        //遍历methods对象上的所有的方法属性
        for (let key in this.$methods) {
            //把每个方法先绑定死 this，然后赋值给this[key] this.say
            this[key] = this.$methods[key].bind(this);
        }
    }
    //数据代理 使得this.msg 等价于this.$data.msg
    proxyData() {
        //遍历this.$data上的所有的属性
        for (let key in this.$data) {
            //vm.msg  this[key]
            //给this也就是当前的Vue类的实例定义key属性
            Object.defineProperty(this, key, {
                get() {//从Vm实例上取值的会去Vm实例上的$data属性上取值
                    return this.$data[key];
                },
                set(newVal) {//给vm实例赋值的时候会把值赋给vm实例的$data对象上
                    this.$data[key] = newVal;
                    //这里问题非常非常大
                    //1.性能问题 不管修改哪一个变量，都需要把页面重新统，性能低下
                    //2.就会生现事件重复绑定的问题，事件越绑越多
                    //3.响应式原理或者说靶向更新，细粒度更新，定向更新
                    //4.更新数据的时候，只会影响使用到该 数据的地方，没用到的地方不需要做任何事件
                    //this.compile(this.$el);
                }
            })
        }
    }
    //编译模板,不但能编译 自己，还能能编译子节点或者说下级节点
    //在编译模板的时候要者依赖的收集。
    compile(el, isOnce = false, scope) {
        //vm默认就是Vue实例，但是你可以传递新的Scope过来，我就会把新的Scope当成vm
        const vm = scope || this;
        //1.获取当前元素的子节点
        const { childNodes } = el;
        //保存条件渲染的元素
        const conditionalElements = [];
        //遍历每一个子节点
        [...childNodes].forEach(node => {
            //如果当前的节点是一个元素节点，并且拥有v-pre属性的话
            if (node.nodeType === ELEMENT_NODE && node.hasAttribute('v-pre')) {
                //跳过自己和自己的子节点的编译过程
                return;
            }
            //如果此子节点的类型是元素节点的话
            if (node.nodeType === ELEMENT_NODE) {
                if (
                    node.hasAttribute('v-if') ||
                    node.hasAttribute('v-else-if') ||
                    node.hasAttribute('v-else')
                ) {
                    conditionalElements.push(node);
                }
                isOnce = isOnce || node.hasAttribute('v-once')
                //编译属性
                compileAttributes([...node.attributes], node, vm, isOnce);
                //编译此子元素节点
                //如果此节点有v-once属性的话，那此此节点下面所有的地方都不再添加Watcher
                //如果处理完指令 后此node还会父节点的下面继续编译，如果已经被 删除了，没必要编译
                if (el.contains(node)) {
                    vm.compile(node, isOnce);
                }

                //如果此子节点是一个文本子节点的话  
            } else if (node.nodeType === TEXT_NODE) {
                //先尝试去获取此文本节点上的originalTextContent原始的文件本内容
                let { originalTextContent } = node;
                //如果没有，说明是第一次渲染
                if (!originalTextContent) {
                    //取取此文本节点的内容
                    originalTextContent = node.textContent;
                    //然后赋给node.originalTextContent属性
                    node.originalTextContent = originalTextContent;
                }
                let reg = /\{\{(.+?)\}\}/g;
                const updateTextContent = () => {
                    node.textContent = originalTextContent.replace(reg, (_, key) => {
                        return JSON.stringify(evaluate(key, vm));
                    });
                }
                updateTextContent();
                //如果不是只渲染一次，就添加监听
                if (!isOnce) {
                    //进行依赖收集，创建观察者，观察counter1这个变量，以后当变量更新的时候，会通知 此处进行更新真实DOM
                    let match;
                    //依次匹配每一个变量
                    while ((match = reg.exec(originalTextContent)) !== null) {
                        //获取到每一个变量名字 counter1,counter2,counter3
                        const key = match[1].trim();
                        //为模板里生一个使用到变量(也就是依赖)增加一个观察者的实例,负责监听此变量的变更
                        //如果以后此变量更新，则会执行回调函数，更新真实DOM
                        //vm Vue实例 key=fullName updateTextContent
                        new Watcher(vm, key, updateTextContent);
                    }
                }
            }
        });
        if (conditionalElements.length > 0) {
            handleConditionalElements(conditionalElements, vm);
        }
        el.removeAttribute('v-cloak');
    }
}
//处理多条件的if指令 conditionalElements是多个if指令所在的元素的数组
function handleConditionalElements(conditionalElements, vm) {
    //上一个显示的元素
    let lastVisibleElement = null;
    //遍历数组
    conditionalElements.forEach(element => {
        //获取此元素的表达式
        let expression = element.getAttribute('v-if') || element.getAttribute('v-else-if') || 'true';
        let evaluate = getEvaluate(expression, vm);
        element.evaluate = evaluate;
        //计算表达式的结果 true就是可以显示，false就是不显示
        const shouldDisplay = evaluate();
        //如果要显示并且还还没有确定要显示的元素的
        if (shouldDisplay && !lastVisibleElement) {
            lastVisibleElement = element;//A
        } else {//其它的元素全部删除
            element.parentNode.removeChild(element);
        }
    });
    //更新可见的元素
    const updateVisibility = () => {
        //遍历数组，找到第一个要显示的元素
        const nextVisibleElement = conditionalElements.find(({ evaluate }) => {
            //计算表达式的结果 true就是可以显示，false就是不显示
            return evaluate();
        });
        //如果找到要显示的新元素，并且新旧元素不相同
        if (nextVisibleElement && nextVisibleElement !== lastVisibleElement) {
            //用新的元素替换掉老元素
            lastVisibleElement.parentNode.replaceChild(nextVisibleElement, lastVisibleElement);
            //让新元素成为老元素
            lastVisibleElement = nextVisibleElement;
        }
    }
    //创建一个观察者，监听Vue实例vm的value属性变化，变化之后执行updateVisibility
    //因为我们不知道表达式里要访问哪个变量，无法在此监听具体的变量
    new Watcher(vm, () => conditionalElements.map(({ evaluate }) => evaluate()), updateVisibility);
}
//计算表达式的值
function evaluate(expression, vm) {
    return getEvaluate(expression, vm).call(vm);
}
//获取一个计算表达式的函数，执行函数才能得到返回值
function getEvaluate(expression, vm) {
    //expression =  msg | upper | prefix | suffix
    return function () {
        //expr = msg, filters =['upper','prefix','suffix']
        const [expr, ...filters] = expression.split('|').map(expr => expr.trim());
        //获取表达式的值
        let value = new Function(`with(this){return ` + expr + `}`).call(vm);
        //把得到的表达式的值自左向右传递给过滤器
        for (const filterName of filters) {
            const filterFunc = Vue.filters[filterName];
            value = filterFunc(value);
        }
        return value;
    }
}
/**
 * 编译属性，识别指令进行处理
 * @param {*} attributes 
 * @param {*} node 
 * @param {*} vm 
 */
function compileAttributes(attributes, node, vm, isOnce) {
    attributes.forEach(attr => {
        //如果属性名是v-text
        if (attr.name === 'v-text') {
            handleVText(node, attr, vm, isOnce);
        } else if (attr.name === 'v-html') {
            handleVHtml(node, attr, vm, isOnce);
            //如果属性名是以v-bind开头的话
        } else if (attr.name === 'v-bind:class' || attr.name === ':class') {
            handleVBindClass(node, attr, vm, isOnce);
        } else if (attr.name === 'v-bind:style' || attr.name === ':style') {
            handleVBindStyle(node, attr, vm, isOnce);
        } else if (attr.name.startsWith('v-bind:') || attr.name.startsWith(':')) {//v-bind:title
            handleVBind(node, attr, vm, isOnce);
            //如果属性名是以v-on开头的话 v-on:click="say"
        } else if (attr.name.startsWith('v-on:') || attr.name.startsWith('@')) {
            handleEvent(node, attr, vm);
        } else if (attr.name === 'v-model') {
            handleVModel(node, attr, vm, isOnce);
        } else if (attr.name === 'v-show') {
            handleVShow(node, attr, vm, isOnce);
        } else if (attr.name === 'v-for') {
            handleVFor(node, attr, vm, isOnce);
        }
    });
}
function handleVBindStyle(node, attr, vm, isOnce) {
    let attrName = attr.name;
    if (attrName.startsWith(':')) {//:title 
        attrName = `v-bind${attrName}`;// v-bind:title
    }
    //获取属性名
    attrName = attrName.slice(7);//style
    function updateStyle() {
        //计算表达式的值，得到样式对象
        const styleObj = evaluate(attr.value, vm);
        //遍历对象每个属性
        for (const [key, value] of Object.entries(styleObj)) {
            //把内联样式赋值给节点
            node.style[key] = value;
        }
    }
    updateStyle();
    new Watcher(vm, attr.value, updateStyle);
}
function handleVBindClass(node, attr, vm, isOnce) {
    //获取属性名 :class  v-bind:class
    let attrName = attr.name;
    if (attrName.startsWith(':')) {//:title 
        attrName = `v-bind${attrName}`;// v-bind:title
    }
    //获取绑定的属性名v-bind:class
    attrName = attrName.slice(7);//class
    //获取属性的值 activeClass,{active:isActive,'text-danger':hasError},[activeClass,errorClass]
    const key = attr.value;
    function updateClassName() {
        let className = '';
        //获取值
        let value = evaluate(key, vm);
        if (Array.isArray(value)) {
            className = value.join(' ');// active error
        } else if (typeof value === 'object') {
            //['active','text-danger']
            className = Object.keys(value).filter(className => value[className]).join(' ')
        } else if (typeof value === 'string') {
            className = value;
        }
        node.setAttribute('class', className);
    }
    updateClassName();
    if (!isOnce) {
        new Watcher(vm, key, updateClassName);
    }
}
function handleVFor(node, attr, vm, isOnce) {
    //先把原始li node节点深度克隆一份,再也不变了
    const originNode = node.cloneNode(true);
    //获取li的父节点ul
    const { parentNode } = node;
    //创建一个占位符
    const placeholder = document.createComment('v-for-placeholder');
    //用注释占位符替换掉老的li节点
    parentNode.replaceChild(placeholder, node);
    //获取v-for属性的值
    const expression = attr.value;//user in users
    //把表达式按空格分割，第1项是条目临时变量名，in写死的忽略，第2项是data对象的属性名
    const [itemVar, , listKey] = expression.split(' ');
    //更新DOM函数
    const update = () => {
        //如果placeholder下面有弟弟的话
        while (placeholder.nextSibling) {
            parentNode.removeChild(placeholder.nextSibling);
        }
        //先获取users数组,然后进行遍历 [{id:1},{id:2},{id:3}]
        evaluate(listKey, vm).forEach((itemData) => {//user
            //根据最初的li克隆一个新的li出来
            const clonedNode = originNode.cloneNode(true);//li
            //然后后克隆后的li添加到ul上
            parentNode.appendChild(clonedNode);//{{item.name}}
            //根据当前的vm创建一个新的作用域对象
            const newScope = Object.create(vm);
            //在里面添加一个属性叫 newScope.user = 用户对象
            newScope[itemVar] = itemData;
            //使用新的作用域编译新节点
            vm.compile(clonedNode, isOnce, newScope);
        });
    }
    //在初始化的时候先渲染一次
    update();
    //以后每当listKey发生变化都会重新更新
    new Watcher(vm, listKey, update);
}
function handleVShow(node, attr, vm, isOnce) {
    //key属性 expr表达式
    //到现在为止，我们使用都是data对象的属性
    const expr = attr.value;
    const originalDisplay = node.style.display;
    //更新 DOM节点的display属性
    const update = () => {
        node.style.display = evaluate(expr, vm) ? originalDisplay : 'none';
    }
    update();
    if (!isOnce) {
        //创建一个Watcher,当vm的expr属性值发生变化的时候重新执行update方法，更新display
        new Watcher(vm, expr, update);
    }
}
/**
 * 处理v-model指令 ，让输入框的值和data数据对象里的属性进行双向绑定
 * @param {*} node 当前的DOM节点，就是那个input输入框
 * @param {*} attr {v-model:'msg'}
 * @param {*} vm Vue实例
 */
function handleVModel(node, attr, vm, isOnce) {
    const key = attr.value;//msg
    //把模型上的值绑定到DOM元素上
    node.value = evaluate(key, vm);//hello
    //监听DOM事件，修改模型的数据
    node.addEventListener('input', () => {
        vm[key] = node.value;
    });
    //在编译的时候，需要添加一个监听器Watcher,监听模型的数据变化，变化之后修改DOM
    //监听Vue实例也就是vm的属性也就是key的变量，当值发生变化后执行cb回调，回调里可以修改DOM节点的值
    if (!isOnce) {
        new Watcher(vm, key, () => {
            node.value = evaluate(key, vm);
        });
    }
}
function handleVText(node, attr, vm, isOnce) {
    //获取属性的值
    const expr = attr.value;//msg
    //从当前的Vue实例上获取msg 属性的值作为当前元素的文本内容 
    node.textContent = evaluate(expr, vm);
    if (!isOnce) {
        new Watcher(vm, expr, (oldValue, newValue) => {
            node.textContent = newValue;
        });
    }
}
function handleVHtml(node, attr, vm, isOnce) {
    //获取属性的值
    const expr = attr.value;//msg
    //从当前的Vue实例上获取msg 属性的值作为当前元素的文本内容 
    node.innerHTML = evaluate(expr, vm);
    if (!isOnce) {
        new Watcher(vm, expr, () => {
            node.innerHTML = evaluate(expr, vm);
        });
    }
}
/**
 * 处理属性绑定
 * @param {*} node DOM节点
 * @param {*} attr 属性对象
 * @param {*} vm Vue实例
 */
function handleVBind(node, attr, vm, isOnce) {
    //获取 属性名 :title  v-bind:title
    let attrName = attr.name;
    if (attrName.startsWith(':')) {//:title 
        attrName = `v-bind${attrName}`;// v-bind:title
    }
    //获取绑定的属性名
    attrName = attrName.slice(7);//title
    //获取属性的值 title
    const key = attr.value;
    //给DOM节点添加属性，属性名叫
    //node.title = '我是标题属性的值'
    node.setAttribute(attrName, evaluate(key, vm));
    if (!isOnce) {
        new Watcher(vm, key, () => {
            node.setAttribute(attrName, evaluate(key, vm));
        });
    }

}
function handleEvent(node, attr, vm) {
    let attrName = attr.name;
    if (attrName.startsWith('@')) {//@click
        attrName = `v-on:${attrName.slice(1)}`;// v-on:click
    }
    //获取要绑定的事件名 v-on:click.stop.prevent
    //第1个元素是事件名，后面的都是修饰符 modifiers=[stop,prevent]
    const [eventName, ...modifiers] = attrName.slice(5).split('.');//[click,stop,prevent]
    //获取 方法表达式 sayHello sayHello( 1, 'a', $event , msg ) 
    const methodExpression = attr.value.trim();
    //先让方法名默认等于methodExpression
    let methodName = methodExpression;
    //方法的形参
    let args = [];
    //查找左小括号的索引
    let parenIndex = methodExpression.indexOf('(');//8
    //说明有小括号,处理参数
    if (parenIndex !== -1) {
        //methodName = sayHello
        methodName = methodExpression.substring(0, parenIndex);
        const argsString = methodExpression.substring(//1, 'a', $event, msg
            parenIndex + 1,
            methodExpression.length - 1
        ).trim();
        //args=[1,'a',$event,msg]
        args = argsString.split(',').map(arg => arg.trim());
    } else {
        //如果没有指定参数，则会传递默认的参数，就是事件对象
        args = ['$event'];
    }
    //这是传递给添加事件监听的选项对象
    let options = {
        capture: modifiers.includes('capture'),
        once: modifiers.includes('once'),
        passive: modifiers.includes('passive')
    }
    //给当前的dom元素绑定点击事件，当点击此DOM元素的时候执行vm.say方法
    node.addEventListener(eventName, (event) => {
        //如果有stop修饰符的话，阻止冒泡
        if (modifiers.includes('stop')) event.stopPropagation();//阻止事件传播，阻止冒泡
        //如果有 prevent修饰符的话，阻止执行默认事件
        if (modifiers.includes('prevent')) event.preventDefault();
        if (modifiers.includes('self')) {//只有在自己身上触发才可以
            //如果事件源对象不是我自己，那么说明此事件是儿子冒泡上的，自己不处理
            if (event.target !== event.currentTarget) {
                return;
            }
        }//KeyCodeMap event.keyCode
        //当前的按键编码
        //获取本次绑定事件的时候指定了哪些按键修饰符 ['prevent','enter','up']
        let modifierKeys = modifiers.map(modifier => KeyMap[modifier]).filter(Boolean).flat();//['Enter']
        //如果发现本次按下的按键在按键修饰符中没有指定，那么直接return
        if (modifierKeys.length > 0) {
            if (!modifierKeys.includes(event.key)) {
                return;
            }
        }
        let actualArgs = args.map(arg => {
            if (!isNaN(arg)) return arg;//如果参数是一个数字，则直接传递
            if (arg === '$event') return event;//如果传递的是一个字符串$event,返回原生的事件地象
            if (arg.startsWith("'") && arg.endsWith("'")) return arg.slice(1, -1);
            return evaluate(arg, vm);
        });

        vm[methodName](...actualArgs);
        //https://developer.mozilla.org/zh-CN/docs/Web/API/EventTarget/addEventListener
    }, options);
}

